45

requestAnimationFrame 方法让我们可以在下一帧开始时调用指定函数。但是很多人可能不知道,不管三七二十一直接在 requestAnimationFrame 的回调函数里绘制动画会有一个问题。是什么问题呢?要理解这个问题,我们先要了解 requestAnimationFrame 的一个知识点。

requestAnimationFrame 不管理回调函数

这个知识点就是 requestAnimationFrame 不管理回调函数。这一点在 w3c 中明确说明了。

Also note that multiple calls to requestAnimationFrame with the same callback (before callbacks are invoked and the list is cleared) will result in multiple entries being in the list with that same callback, and thus will result in that callback being invoked more than once for the animation frame.
w3c

即在回调被执行前,多次调用带有同一回调函数的 requestAnimationFrame,会导致回调在同一帧中执行多次。我们可以通过一个简单的例子模拟在同一帧内多次调用 requestAnimationFrame 的场景:

const animation = timestamp => console.log('animation called at', timestamp)

window.requestAnimationFrame(animation)
window.requestAnimationFrame(animation)
// animation called at 320.7559999991645
// animation called at 320.7559999991645

我们用连续调用两次 requestAnimationFrame 模拟在同一帧中调用两次 requestAnimationFrame

例子中的 timestamp 是由 requestAnimationFrame 传给回调函数的,表示回调队列被触发的时间。由输出可知,animation 函数在同一帧内被执行了两次,即绘制了两次动画。然而在同一帧绘制两次动画很明显是多余的,相当于画了一幅画,然后再在这幅画上再画上同样的一幅画。

问题

那么什么场景下,requestAnimationFrame 会在一帧内被多次调用呢?熟悉事件的同学应该马上能想到 mousemove, scroll 这类事件。

所以前面我们提到的问题就是:因为 requestAnimationFrame 不管理回调函数,在滚动、触摸这类高触发频率的事件回调里,如果调用 requestAnimationFrame 然后绘制动画,可能会造成多余的计算和绘制。例如:

window.addEventListener('scroll', e => {
    window.requestAnimationFrame(timestamp => {
        animation(timestamp)
    })
})

在上面代码中,scroll 事件可能在一帧内多次触发,所以 animation 函数可能会在一帧内重复绘制,造成不必要的计算和渲染。

解决方法

对于这种高频发事件,一般的解决方法是使用节流函数。但是在这里使用节流函数并不能完美解决问题。因为节流函数是通过时间管理队列的,而 requestAnimationFrame 的触发时间是不固定的,在高刷新频率的显示屏上时间会小于 16.67ms,页面如果被推入后台,时间可能大于 16.67ms。

完美的解决方案是通过 requestAnimationFrame 来管理队列,其思路就是保证 requestAnimationFrame 的队列里,同样的回调函数只有一个。示意代码如下:

const onScroll = e => {
    if (scheduledAnimationFrame) { return }

    scheduledAnimationFrame = true
    window.requestAnimationFrame(timestamp => {
        scheduledAnimationFrame = false
        animation(timestamp)
    })
}
window.addEventListener('scroll', onScroll)

但是每次都要写这么一堆代码,也有点麻烦。所以我开源了 raf-plus 库用于解决这个问题,有需要的的同学可以用用~

结论

requestAnimationFrame 不管理回调函数队列,而滚动、触摸这类高触发频率事件的回调可能会在同一帧内触发多次。所以正确使用 requestAnimationFrame 的姿势是,在同一帧内可能调用多次 requestAnimationFrame 时,要管理回调函数,防止重复绘制动画。


前端菜鸟
244 声望6 粉丝